// Copyright 2012 Google Inc. All Rights Reserved.

package com.google.appengine.syncserver.entities;

import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.EntityNotFoundException;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.Transaction;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

/**
 * A class to store key-value pairs in datastore.
 */
public class Preferences {

  private boolean dirtyBit;

  /**
   * Sharded preferences representing the key-value pairs.
   */
  private final ShardedPreferences shardedPreferences;

  /**
   * Metadata of the sharded preferences.
   */
  private final PreferencesMetadata metadata;

  /**
   * Username to which preferences belongs to.
   */
  private final String username;

  /**
   * Preferences name as in Android
   */
  private final String preferencesName;

  private final DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
  private final Transaction transaction = datastore.beginTransaction();

  /**
   * Kind of the underlying preferences entity.
   */
  private final EntityKeyFactory.PreferencesEntityKind preferencesKind;

  /**
   * @param username
   * @param preferencesName
   * @param kind The kind of underlying datastore entity.
   */
  public Preferences(String username, String preferencesName,
      EntityKeyFactory.PreferencesEntityKind kind) throws Exception {
    this.username = username;
    this.preferencesName = preferencesName;
    this.preferencesKind = kind;
    this.dirtyBit = false;
    // Get metadata entity from datastore.
    this.metadata = getMetadataEntityFromDatastore();
    this.shardedPreferences = new ShardedPreferences(getShardedEntitiesFromDatastore(
        metadata.getNumberOfParts()), metadata.getMaxVersion());
  }

  public void commit() throws Exception {

    // Dont write to datastore if nothing has changed.
    if (!dirtyBit) {
      return;
    }

    List<Entity> entities = shardedPreferences.getEntities(
        username, preferencesKind, preferencesName);

    long oldNoOfParts = metadata.getNumberOfParts();

    // Set the number of parts in the metadata.
    metadata.setNumberOfParts(entities.size());
    // Set the max version number.
    metadata.setMaxVersion(shardedPreferences.getMaxVersion());
    entities.add(metadata.getEntity());
    datastore.put(transaction, entities);

    ArrayList<Key> removeKeys = new ArrayList<Key>();
    // Make a list of shards to be removed from datastore. This is to
    // account for the fact that you could have had more shards in the datastore
    // due to large preferences object earlier.
    for (int i = entities.size(); i < oldNoOfParts; i++) {
      removeKeys.add(
          EntityKeyFactory.getShardedEntityKey(username, preferencesKind, preferencesName, i));
    }

    datastore.delete(transaction, removeKeys);

    transaction.commit();
  }

  /**
   * Get entities which have update version greater than version.
   *
   * @param version
   * @param approximateSize
   * @return Returns a map whose entries have an update version greater than
   *         the value.
   */
  public Map<String, PreferencesValue> getMapForVersion(long version, int approximateSize) {
    return shardedPreferences.getProperties(version, approximateSize);
  }

  /**
   * @return maximum version for the entity.
   */
  public long getMaxVersion() {
    return shardedPreferences.getMaxVersion();
  }

  /**
   * @return html string.
   */
  public String toHtmlString() {
    return shardedPreferences.toHtmlString();
  }

  /**
   * @param key
   * @return Value corresponding to the key.
   */
  public PreferencesValue getProperty(String key) {
    return shardedPreferences.getProperty(key);
  }

  /**
   * @param key
   */
  public void removeProperty(String key) {
    dirtyBit = true;
    shardedPreferences.removeProperty(key);
  }

  /**
   * @param key
   * @param value
   */
  public void setProperty(String key, PreferencesValue value) {
    dirtyBit = true;
    shardedPreferences.setProperty(key, value);
  }

  /**
   * Get the metadata from the datastore for this preferences.
   *
   * @return The metadata object constructed using the metadata entity from datastore.
   */
  private PreferencesMetadata getMetadataEntityFromDatastore() {
    Entity entity;
    Key key = EntityKeyFactory.getMetadataEntityKey(username, preferencesKind, preferencesName);
    try {
      entity = datastore.get(key);
    } catch (EntityNotFoundException e) {
      // Not in datastore. Create a new entity.
      entity = new Entity(key);
    }
    return new PreferencesMetadata(entity);
  }

  /**
   * @param parts the number of shared preferences parts.
   * @return Collection list of entities for the sharded preferences file.
   */
  private Collection<Entity> getShardedEntitiesFromDatastore(long parts) {
    if (parts == 0) {
      return new ArrayList<Entity>();
    }

    List<Key> keys = new ArrayList<Key>();
    // Batch all keys to get in a list.
    for (int i = 0; i < parts; i++) {
      keys.add(EntityKeyFactory.getShardedEntityKey(username, preferencesKind, preferencesName, i));
    }

    // Return an array list from all the returned values.
    return datastore.get(transaction, keys).values();
  }

  /**
   * clear shared preferences and also clears the underlying metadata.
   */
  protected void clear() {
    dirtyBit = true;
    shardedPreferences.clear();
  }

  /**
   * Removes the metadata from the datastore. Can be used when you want purge all the entities
   * related to the shared preferences.
   */
  protected void removeMetadata() {
    dirtyBit = true;
    datastore.delete(transaction,
        EntityKeyFactory.getMetadataEntityKey(username, preferencesKind, preferencesName));
  }

  /**
   * Roll back if active.
   */
  public void rollBackIfActive() {
    if (transaction.isActive()) {
      transaction.rollback();
    }
  }

  /**
   * get all properties.
   */
  public Map<String, PreferencesValue> getAllProperties() {
    return shardedPreferences.getAllProperties();
  }
}
